]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
extensions-app: Use ListModel to track extensions
authorFlorian Müllner <fmuellner@gnome.org>
Tue, 19 Dec 2023 17:57:37 +0000 (18:57 +0100)
committerMarge Bot <marge-bot@gnome.org>
Thu, 21 Dec 2023 15:24:23 +0000 (15:24 +0000)
Manually adding and removing rows to dynamic lists is rather
old-fashioned, GTK 4 strongly encourages the use of models.

Modernize the code by exposing extensions as ListModel, and
bind it to the two lists with appropriate filters.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3067>

subprojects/extensions-app/data/ui/extensions-window.ui
subprojects/extensions-app/js/extensionManager.js
subprojects/extensions-app/js/extensionsWindow.js

index 7047f130c83a65b803d819aa2c2054fe0b846a0d..ba8470ce3edc96ea4d7efdd0445da70590126eed 100644 (file)
       </item>
     </section>
   </menu>
+  <object class="GtkSortListModel" id="sortModel">
+    <property name="sorter">
+      <object class="GtkStringSorter">
+        <property name="expression">
+          <lookup name="name" type="Extension"/>
+        </property>
+      </object>
+    </property>
+  </object>
+  <object class="GtkStringFilter" id="searchFilter">
+    <property name="ignore-case">true</property>
+    <property name="match-mode">substring</property>
+    <property name="expression">
+      <lookup name="name" type="Extension"/>
+    </property>
+  </object>
+  <object class="GtkFilterListModel" id="userListModel">
+    <property name="model">sortModel</property>
+    <property name="filter">
+      <object class="GtkBoolFilter">
+        <property name="expression">
+          <lookup name="is-user" type="Extension"/>
+        </property>
+      </object>
+    </property>
+  </object>
+  <object class="GtkFilterListModel" id="systemListModel">
+    <property name="model">sortModel</property>
+    <property name="filter">
+      <object class="GtkBoolFilter">
+        <property name="invert">true</property>
+        <property name="expression">
+          <lookup name="is-user" type="Extension"/>
+        </property>
+      </object>
+    </property>
+  </object>
   <template class="ExtensionsWindow" parent="AdwApplicationWindow">
     <property name="default-width">800</property>
     <property name="default-height">500</property>
                         <child>
                           <object class="AdwPreferencesGroup" id="userGroup">
                             <property name="title" translatable="yes">User Extensions</property>
+                            <property name="visible"
+                                      bind-source="userListModel"
+                                      bind-property="n-items"
+                                      bind-flags="sync-create"/>
                             <child>
                               <object class="GtkListBox" id="userList">
                                 <property name="selection-mode">none</property>
                         <child>
                           <object class="AdwPreferencesGroup" id="systemGroup">
                             <property name="title" translatable="yes">System Extensions</property>
+                            <property name="visible"
+                                      bind-source="systemListModel"
+                                      bind-property="n-items"
+                                      bind-flags="sync-create"/>
                             <child>
                               <object class="GtkListBox" id="systemList">
                                 <property name="selection-mode">none</property>
index 7671fb75e1ba833e9630646d94cc00c3f15d4406..0824c99c70482718cbfffa27fb22c76736040bed 100644 (file)
@@ -108,8 +108,6 @@ const Extension = GObject.registerClass({
 
         const {name} = metadata;
         if (this._name !== name) {
-            [this._keywords] = GLib.str_tokenize_and_fold(name, null);
-
             this._name = name;
             this.notify('name');
         }
@@ -207,10 +205,6 @@ const Extension = GObject.registerClass({
         return this._version;
     }
 
-    get keywords() {
-        return this._keywords;
-    }
-
     get error() {
         if (!this.hasError)
             return '';
@@ -268,6 +262,10 @@ export const ExtensionManager = GObject.registerClass({
             'user-extensions-enabled', null, null,
             GObject.ParamFlags.READWRITE,
             true),
+        'extensions': GObject.ParamSpec.object(
+            'extensions', null, null,
+            GObject.ParamFlags.READABLE,
+            Gio.ListModel),
         'n-updates': GObject.ParamSpec.int(
             'n-updates', null, null,
             GObject.ParamFlags.READABLE,
@@ -278,16 +276,13 @@ export const ExtensionManager = GObject.registerClass({
             false),
     },
     Signals: {
-        'extension-added': {param_types: [Extension]},
-        'extension-removed': {param_types: [Extension]},
-        'extension-changed': {param_types: [Extension], flags: GObject.SignalFlags.DETAILED},
         'extensions-loaded': {},
     },
 }, class ExtensionManager extends GObject.Object {
     constructor() {
         super();
 
-        this._extensions = new Map();
+        this._extensions = new Gio.ListStore({itemType: Extension});
 
         this._proxyReady = false;
         this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
@@ -312,6 +307,10 @@ export const ExtensionManager = GObject.registerClass({
         this._loadExtensions().catch(console.error);
     }
 
+    get extensions() {
+        return this._extensions;
+    }
+
     get userExtensionsEnabled() {
         return this._shellProxy.UserExtensionsEnabled ?? false;
     }
@@ -322,7 +321,7 @@ export const ExtensionManager = GObject.registerClass({
 
     get nUpdates() {
         let nUpdates = 0;
-        for (const ext of this._extensions.values()) {
+        for (const ext of this._extensions) {
             if (ext.isUser && ext.hasUpdate)
                 nUpdates++;
         }
@@ -355,43 +354,37 @@ export const ExtensionManager = GObject.registerClass({
         this._shellProxy.CheckForUpdatesAsync().catch(console.error);
     }
 
-    _addExtension(extension) {
-        const {uuid} = extension;
-        if (this._extensions.has(uuid))
-            return;
-
-        this._extensions.set(uuid, extension);
-        this.emit('extension-added', extension);
-    }
-
-    _removeExtension(extension) {
-        const {uuid} = extension;
-        if (this._extensions.delete(uuid))
-            this.emit('extension-removed', extension);
-    }
-
     async _loadExtensions() {
         const [extensionsMap] = await this._shellProxy.ListExtensionsAsync();
 
         for (let uuid in extensionsMap) {
             const extension = new Extension(extensionsMap[uuid]);
-            this._addExtension(extension);
+            this._extensions.append(extension);
         }
         this.emit('extensions-loaded');
     }
 
+    _findExtension(uuid) {
+        const len = this._extensions.get_n_items();
+        for (let pos = 0; pos < len; pos++) {
+            const extension = this._extensions.get_item(pos);
+            if (extension.uuid === uuid)
+                return [extension, pos];
+        }
+
+        return [null, -1];
+    }
+
     _onExtensionStateChanged(p, sender, [uuid, newState]) {
-        const extension = this._extensions.get(uuid);
+        const [extension, pos] = this._findExtension(uuid);
 
         if (extension)
             extension.update(newState);
 
         if (!extension)
-            this._addExtension(new Extension(newState));
+            this._extensions.append(new Extension(newState));
         else if (extension.state === ExtensionState.UNINSTALLED)
-            this._removeExtension(extension);
-        else
-            this.emit(`extension-changed::${uuid}`, extension);
+            this._extensions.remove(pos);
 
         if (this._updatesCheckId)
             return;
index 4af3630ea93c3be5b8729ce546a24b0fc486bd9a..03e9d8be879aa4fcac395580d47b615086226e9f 100644 (file)
@@ -18,6 +18,10 @@ export const ExtensionsWindow = GObject.registerClass({
     GTypeName: 'ExtensionsWindow',
     Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
     InternalChildren: [
+        'sortModel',
+        'searchFilter',
+        'userListModel',
+        'systemListModel',
         'userGroup',
         'userList',
         'systemGroup',
@@ -54,20 +58,9 @@ export const ExtensionsWindow = GObject.registerClass({
                 },
             }]);
 
-        this._searchTerms = [];
-        this._searchEntry.connect('search-changed', () => {
-            const {text} = this._searchEntry;
-            if (text === '')
-                this._searchTerms = [];
-            else
-                [this._searchTerms] = GLib.str_tokenize_and_fold(text, null);
+        this._searchEntry.connect('search-changed',
+            () => (this._searchFilter.search = this._searchEntry.text));
 
-            this._userList.invalidate_filter();
-            this._systemList.invalidate_filter();
-        });
-
-        this._userList.set_sort_func(this._sortList.bind(this));
-        this._userList.set_filter_func(this._filterList.bind(this));
         this._userList.set_placeholder(new Gtk.Label({
             label: _('No Matches'),
             margin_start: 12,
@@ -76,9 +69,8 @@ export const ExtensionsWindow = GObject.registerClass({
             margin_bottom: 12,
         }));
         this._userList.connect('row-activated', (_list, row) => row.activate());
+        this._userGroup.connect('notify::visible', () => this._syncVisiblePage());
 
-        this._systemList.set_sort_func(this._sortList.bind(this));
-        this._systemList.set_filter_func(this._filterList.bind(this));
         this._systemList.set_placeholder(new Gtk.Label({
             label: _('No Matches'),
             margin_start: 12,
@@ -87,6 +79,7 @@ export const ExtensionsWindow = GObject.registerClass({
             margin_bottom: 12,
         }));
         this._systemList.connect('row-activated', (_list, row) => row.activate());
+        this._systemGroup.connect('notify::visible', () => this._syncVisiblePage());
 
         const {extensionManager} = this.application;
         extensionManager.connect('notify::failed',
@@ -97,19 +90,16 @@ export const ExtensionsWindow = GObject.registerClass({
             this._onUserExtensionsEnabledChanged.bind(this));
         this._onUserExtensionsEnabledChanged();
 
-        extensionManager.connect('extension-added',
-            (mgr, extension) => this._addExtensionRow(extension));
-        extensionManager.connect('extension-removed',
-            (mgr, extension) => this._removeExtensionRow(extension));
-        extensionManager.connect('extension-changed',
-            (mgr, extension) => {
-                const row = this._findExtensionRow(extension);
-                const isUser = row?.get_parent() === this._userList;
-                if (extension.isUser !== isUser) {
-                    this._removeExtensionRow(extension);
-                    this._addExtensionRow(extension);
-                }
-            });
+        this._sortModel.model = extensionManager.extensions;
+
+        this._userList.bind_model(new Gtk.FilterListModel({
+            filter: this._searchFilter,
+            model: this._userListModel,
+        }), extension => new ExtensionRow(extension));
+        this._systemList.bind_model(new Gtk.FilterListModel({
+            filter: this._searchFilter,
+            model: this._systemListModel,
+        }), extension => new ExtensionRow(extension));
 
         extensionManager.connect('extensions-loaded',
             () => this._extensionsLoaded());
@@ -189,56 +179,12 @@ export const ExtensionsWindow = GObject.registerClass({
             null);
     }
 
-    _sortList(row1, row2) {
-        const {name: name1} = row1.extension;
-        const {name: name2} = row2.extension;
-        return name1.localeCompare(name2);
-    }
-
-    _filterList(row) {
-        const {keywords} = row.extension;
-        return this._searchTerms.every(
-            t => keywords.some(k => k.startsWith(t)));
-    }
-
-    _findExtensionRow(extension) {
-        return [
-            ...this._userList,
-            ...this._systemList,
-        ].find(c => c.extension === extension);
-    }
-
     _onUserExtensionsEnabledChanged() {
         const {userExtensionsEnabled} = this.application.extensionManager;
         const action = this.lookup_action('user-extensions-enabled');
         action.set_state(new GLib.Variant('b', userExtensionsEnabled));
     }
 
-    _addExtensionRow(extension) {
-        const row = new ExtensionRow(extension);
-
-        if (extension.isUser)
-            this._userList.append(row);
-        else
-            this._systemList.append(row);
-
-        this._syncListVisibility();
-    }
-
-    _removeExtensionRow(extension) {
-        const row = this._findExtensionRow(extension);
-        if (row)
-            row.get_parent().remove(row);
-        this._syncListVisibility();
-    }
-
-    _syncListVisibility() {
-        this._userGroup.visible = [...this._userList].length > 1;
-        this._systemGroup.visible = [...this._systemList].length > 1;
-
-        this._syncVisiblePage();
-    }
-
     _syncVisiblePage() {
         const {extensionManager} = this.application;
 
@@ -261,7 +207,7 @@ export const ExtensionsWindow = GObject.registerClass({
     }
 
     _extensionsLoaded() {
-        this._syncListVisibility();
+        this._syncVisiblePage();
         this._checkUpdates();
     }
 });